/*
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ctakit.kiwi.web.view;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.ctakit.kiwi.common.core.object.ResponseResult;
import com.ctakit.kiwi.common.core.util.Resources;
import com.ctakit.kiwi.common.core.util.WebConstants;
import com.ctakit.kiwi.web.util.RequestUtils;
import com.ctakit.kiwi.web.util.ThreadLocalUtils;
import com.fasterxml.jackson.core.util.ByteArrayBuilder;
import lombok.Data;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import java.util.Set;

/**
 * Spring-MVC {@link View} that renders JSON content by serializing the model
 * for the current request using <a
 * href="http://jackson.codehaus.org/">Jackson's</a> {@link }.
 *
 * <p>
 * By default, the entire contents of the model map (with the exception of
 * framework-specific classes) will be encoded as JSON. For cases where the
 * contents of the map need to be filtered, users may specify a specific set of
 * model attributes to encode via the {@link #setRenderedAttributes(Set)
 * renderedAttributes} property.
 *
 * @author Jeremy Grelle
 * @author Arjen Poutsma
 * @since 3.0
 */
@Data
public class MyMappingJacksonJsonView extends AbstractView {

    private static final boolean devMode = Resources.getBoolean("devMode", false);

    /**
     * Default content type. Overridable as bean property.
     */
    public static final String DEFAULT_CONTENT_TYPE = "application/json; charset=UTF-8";

    // private ObjectMapper objectMapper = new ObjectMapper();

    // private JsonEncoding encoding = JsonEncoding.UTF8;

    // private boolean prefixJson = false;

    private Set<String> renderedAttributes;

    private boolean disableCaching = true;

    private boolean updateContentLength = false;

    /**
     * Construct a new {@code JacksonJsonView}, setting the content type to
     * {@code application/json}.
     */
    public MyMappingJacksonJsonView() {
        setContentType(DEFAULT_CONTENT_TYPE);
        setExposePathVariables(false);
    }

    /**
     * Sets the {@code JsonEncoding} for this converter. By default,
     * {@linkplain JsonEncoding#UTF8 UTF-8} is used.
     */
    /*
     * public void setEncoding(JsonEncoding encoding) { Assert.notNull(encoding,
     * "'encoding' must not be null"); this.encoding = encoding; }
     */

    /**
     * Indicates whether the JSON output by this view should be prefixed with "
     * {@code &&}". Default is false.
     *
     * <p>
     * Prefixing the JSON string in this manner is used to help prevent JSON
     * Hijacking. The prefix renders the string syntactically invalid as a
     * script so that it cannot be hijacked. This prefix does not affect the
     * evaluation of JSON, but if JSON validation is performed on the string,
     * the prefix would need to be ignored.
     */
    /*
     * public void setPrefixJson(boolean prefixJson) { this.prefixJson =
     * prefixJson; }
     */
    @Override
    protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
        response.setContentType(getContentType());
        response.setCharacterEncoding(WebConstants.SYSTEM_CHARSET);
        if (disableCaching) {
            response.addHeader("Pragma", "no-cache");
            response.addHeader("Cache-Control", "no-cache, no-store, max-age=0");
            response.addDateHeader("Expires", 1L);
        }
    }

    /*
     * @Override protected void renderMergedOutputModel(Map<String, Object>
     * model, HttpServletRequest request, HttpServletResponse response) throws
     * Exception { Object value = filterModel(model,request);
     * ServletOutputStream out = response.getOutputStream(); JsonGenerator
     * generator = objectMapper.getJsonFactory().createJsonGenerator(out,
     * encoding); if (prefixJson) { generator.writeRaw("{} && "); } String
     * jsonpcallback = request.getParameter("jsoncallback"); String callback =
     * request.getParameter("callback"); String scriptcallback =
     * request.getParameter("scriptcallback"); if (jsonpcallback != null) {
     * out.write((jsonpcallback + "(").getBytes());
     * objectMapper.writeValue(generator, value); out.write(")".getBytes()); }
     * else if (callback != null) { out.write((callback + "(").getBytes());
     * objectMapper.writeValue(generator, value); out.write(")".getBytes()); }
     * else if (scriptcallback != null) { response.setHeader("Content-Type",
     * "text/html; charset=utf-8"); out.write(("<script \"text/javascript\">" +
     * scriptcallback + "(").getBytes()); objectMapper.writeValue(generator,
     * value); out.write(")</script>".getBytes()); } else {
     * objectMapper.writeValue(generator, value); } }
     */
    @Override
    protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) {
        OutputStream stream = null;
        try {
            stream = (this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream());
            Map<String, Object> value = filterModel(model, request);
            writeContent(stream, value, request, response);
            if (this.updateContentLength) {
                writeToResponse(response, (ByteArrayOutputStream) stream);
            }
        } catch (IOException e) {

        }

    }

    /**
     * Write the actual JSON content to the stream.
     *
     * @param stream the output stream to use
     * @param value  the value to be rendered, as returned from
     *               {@link #filterModel}
     * @throws IOException if writing failed
     */
    protected void writeContent(OutputStream stream, Map<String, Object> value, HttpServletRequest request, HttpServletResponse response) throws IOException {
        /*
         * JsonGenerator generator =
         * JsonUtils.objectMapper.getJsonFactory().createJsonGenerator(stream,
         * JsonEncoding.UTF8); if
         * (JsonUtils.objectMapper.getSerializationConfig(
         * ).isEnabled(SerializationConfig.Feature.INDENT_OUTPUT)) {
         * generator.useDefaultPrettyPrinter(); }
         */
        ByteArrayBuilder valueBytes = new ByteArrayBuilder();
        try {
            String end = null;
            String jsonpcallback = request.getParameter("jsonpcallback");
            String callback = request.getParameter("callback");
            String scriptcallback = request.getParameter("scriptcallback");
            String jsonTextPlain = (String) request.getAttribute("jsonTextPlain");
            ResponseResult<Object> ajaxResult = (ResponseResult<Object>) request.getAttribute("ajaxResult");
            String debug = request.getParameter("debug");
            if (ajaxResult != null) {
                if (devMode || debug != null) {
                    ajaxResult.setRequestArgs(RequestUtils.getQueryParams(request));
                }
                valueBytes.write(JSON.toJSONBytes((ajaxResult)));

            } else {
                if (jsonpcallback != null) {
                    setContentType("text/plain; charset=UTF-8");
                    valueBytes.write((jsonpcallback + "(").getBytes());
                    end = ");";
                } else if (callback != null) {
                    setContentType("text/plain; charset=UTF-8");
                    valueBytes.write((callback + "(").getBytes());
                    end = ");";
                } else if (scriptcallback != null) {
                    setContentType("text/html; charset=UTF-8");
                    valueBytes.write(("<script type=\"text/javascript\">" + scriptcallback + "(").getBytes());
                    end = ");</script>";
                }
                if (StrUtil.isBlank(jsonTextPlain)) {
                    if (!value.isEmpty()) {
                        valueBytes.write(JSON.toJSONBytes((value)));
                    }
                } else {
                    valueBytes.write(jsonTextPlain.getBytes());
                }

                if (StrUtil.isNotBlank(end)) {
                    valueBytes.write(end.getBytes());
                }
            }
            stream.write(valueBytes.toByteArray());
            stream.flush();
        } finally {
            valueBytes.close();
        }

    }

    /**
     * Filters out undesired attributes from the given model. The return value
     * can be either another {@link Map}, or a single value object.
     *
     * <p>
     * Default implementation removes {@link BindingResult} instances and
     * entries not included in the {@link #setRenderedAttributes(Set)
     * renderedAttributes} property.
     *
     * @param model   the model, as passed on to {@link #renderMergedOutputModel}
     * @param request
     * @return the object to be rendered
     */
    protected Map<String, Object> filterModel(Map<String, Object> model, HttpServletRequest request) {
        Map<String, Object> result = RequestUtils.getRequestAttributeMapForModel(request);
        result.putAll(ThreadLocalUtils.getData());
        return result;
    }

}
